<template>
	<div
		class="__avatar_body"
		:style="style"
		:class="{
			__grid_bg: innerConfig.gridBackground,
			__edge_line: innerConfig.edgeLine,
			__opaque_cropper: cropperOpaque,
		}">
		<!-- 图片容器 -->
		<div class="__image_container">
			<img
				v-show="src"
				ref="img"
				alt="image"
				@wheel="__onWheel"
				@wheel.once="__onMouseMove"
				@mousedown="__onMouseDown"
				@mousemove="__onMouseMove"
				:src="src"
				:style="{
					width: __width + 'px',
					height: __height + 'px',
					'--x': x + 'px',
					'--y': y + 'px',
				}" />
		</div>
		<!-- 遮挡层 -->
		<div @mousedown="__onCropperMouseDown" class="__cropper top"></div>
		<div @mousedown="__onCropperMouseDown" class="__cropper right"></div>
		<div @mousedown="__onCropperMouseDown" class="__cropper bottom"></div>
		<div @mousedown="__onCropperMouseDown" class="__cropper left"></div>
		<!-- loading -->
		<transition name="__loading">
			<div v-show="loading === 'pending' || loading === 'error'" class="__loading_mask">
				<div class="__loading_inner">
					<div class="__loading_spinner" v-if="loading === 'pending'">
						<svg viewBox="25 25 50 50" class="__circular">
							<circle cx="50" cy="50" r="20" fill="none"></circle>
						</svg>
					</div>
					<p class="__loading_text" v-if="loading === 'pending'">{{ innerConfig.loadingText }}</p>
					<p class="__loading_text __error" v-if="loading === 'error'">{{ innerConfig.loadingErrorText }}</p>
				</div>
			</div>
		</transition>
	</div>
</template>

<script lang="ts">
import Vue, { PropOptions } from 'vue';
import { fileToBase64, extMap, formatCSSLength } from './utils';
import { Config, Orientation, DataType, Result, LoadingType, ImageFormat } from './type';
import { defaultConfig, _Config } from './defaultConfig';

export default Vue.extend({
	name: 'avatar-body',
	props: {
		config: Object as PropOptions<Config>,
		width: String,
		height: String,
	},
	data() {
		return {
			innerConfig: {} as _Config,
			// 样式
			style: '',
			// 图片 base64 数据
			src: '',
			// 文件名
			filename: '',
			// 图片初始宽高
			initialWidth: 0,
			initialHeight: 0,
			// 图片真实宽高
			naturalWidth: 0,
			naturalHeight: 0,
			// 图片 x, y 坐标
			x: 0,
			y: 0,
			// 图片缩放比例
			scale: 0,
			// 图片最大缩放比例（在limitImageMinSize配置项生效时生效）
			maxScale: 0,
			// 缩放速度
			zoomSpeed: 0.05,
			// 快速缩放速度（当缩放高频触发时）
			rapidZoomSpeed: 0.2,
			// 是否处于快速缩放
			rapidZoom: false,
			// 移动速度
			moveSpeed: 1,
			// 加载状态
			loading: 'success' as LoadingType,
			// 是否在拖动中
			drag: false,
			// 矫正过渡时间
			CTT: 100,
			// 矫正等待时间
			CWT: 100,
			// 遮挡区是否为不透明
			cropperOpaque: false,

			// 定时器存储
			timer1: -1, // 矫正位置延时
			timer2: -1, // 矫正位置时长
			timer3: -1, // 快速缩放
			timer4: -1, // 遮挡区鼠标按下延时

			// 鼠标状态
			mouseState: {
				// 缩放偏移百分比
				zoomOffsetX: 0.5,
				zoomOffsetY: 0.5,
				// 鼠标按下位置
				downX: 0,
				downY: 0,
			},
		};
	},
	created() {
		this.__initConfig();
	},
	methods: {
		// 初始化
		async __init(file: File) {
			if (this.innerConfig.loading) this.loading = 'pending';
			// 初始化值
			this.src = '';
			this.scale = this.x = this.y = this.maxScale = 0;

			const base64 = await fileToBase64(file);

			this.filename = file.name;
			this.src = base64;

			this.$nextTick(() => {
				const dom = this.$refs.img as HTMLImageElement;

				// 移除事件
				const removeEvent = () => {
					dom.onload = null;
					dom.onerror = null;
				};

				dom.onload = () => {
					if (this.innerConfig.loading) this.loading = 'success';
					// 获取真实宽高
					this.naturalWidth = dom.naturalWidth;
					this.naturalHeight = dom.naturalHeight;

					this.__initialPosition();

					removeEvent();
				};
				dom.onerror = () => {
					if (this.innerConfig.loading) this.loading = 'error';

					console.error('Image loading failed');

					removeEvent();
				};
			});
		},
		// 计算初始位置
		__initialPosition() {
			// 图片的宽是否大于高
			const bool = this.naturalWidth > this.naturalHeight;
			const max = Math.max(this.naturalWidth, this.naturalHeight);
			const min = Math.min(this.naturalWidth, this.naturalHeight);

			// 缩放比
			const scale = this.innerConfig.viewfinderSize / min;
			// 图片宽高大的一方的长度
			const len = Math.floor(max * scale);
			// 图片宽高大的一方的偏移量
			const offset = -((len - this.innerConfig.viewfinderSize) / 2);

			// 设置宽高和位置
			if (bool) {
				this.initialWidth = len;
				this.initialHeight = this.innerConfig.viewfinderSize;
				this.x = offset;
				this.y = 0;
			} else {
				this.initialWidth = this.innerConfig.viewfinderSize;
				this.initialHeight = len;
				this.x = 0;
				this.y = offset;
			}

			// 计算最大缩放比例
			const s = this.innerConfig.limitImageMinSize;
			if (s > 0 && min > s) {
				this.maxScale = min / s - 1;
			}
		},
		// 初始化配置
		__initConfig() {
			Object.assign(this.innerConfig, defaultConfig);

			if (this.width) this.innerConfig.width = this.width;
			if (this.height) this.innerConfig.height = this.height;

			Object.assign(this.innerConfig, this.config);

			this.__generateStyle();

			if (this.innerConfig.immediateLoading) this.loading = 'pending';
		},
		// 生成 style
		__generateStyle() {
			this.style = `--CTT: ${this.CTT}ms;
				--body-width: ${formatCSSLength(this.innerConfig.width)};
        --body-height: ${formatCSSLength(this.innerConfig.height)};
        --viewfinder-size: ${this.innerConfig.viewfinderSize}px;`;

			if (this.innerConfig.backgroundColor) this.style += `--body-bg-color: ${this.innerConfig.backgroundColor};`;
			if (this.innerConfig.cropperColor) this.style += `--cropper-color: ${this.innerConfig.cropperColor};`;
			if (this.innerConfig.cropperOpaqueColor)
				this.style += `--cropper-opaque-color: ${this.innerConfig.cropperOpaqueColor};`;
			if (this.innerConfig.edgeLineColor) this.style += `--edge-line-color: ${this.innerConfig.edgeLineColor};`;
		},
		/**
		 * 缩放
		 * @param s 放大/缩小
		 */
		__zoom(s: boolean | number, zoX?: number, zoY?: number) {
			clearTimeout(this.timer3);
			this.__transitionClass();

			let exceeded = false;

			zoX = typeof zoX === 'number' ? zoX : this.mouseState.zoomOffsetX;
			zoY = typeof zoY === 'number' ? zoY : this.mouseState.zoomOffsetY;

			// 存储原始宽高
			const width = this.__width;
			const height = this.__height;

			if (typeof s === 'number') {
				this.scale += s;
			} else {
				// 如果滚动频率处于高的状态，则使用快速缩放速度
				const offset = this.rapidZoom ? this.rapidZoomSpeed : this.zoomSpeed;

				if (s) {
					this.scale += offset; // 放大
				} else {
					this.scale -= offset; // 缩小
				}
			}

			// 防止过度缩放
			if (this.scale < 0) {
				this.scale = 0;
				exceeded = true;
			} else if (this.scale > this.innerConfig.maxZoomRatio) {
				this.scale = this.innerConfig.maxZoomRatio;
				exceeded = true;
			}

			// 当 minAvatarSizeLimit 为 true 且设置了 minAvatarSize 时，图片放大时宽高的最小值就不能超过 minAvatarSize
			if (this.maxScale > 0 && this.scale > this.maxScale) {
				this.scale = this.maxScale;
				exceeded = true;
			}

			// 是否以图片的某点中心缩放
			if (!exceeded) {
				// 计算偏移量（保留两位小数）
				let x = (this.__width - width) * zoX;
				let y = (this.__height - height) * zoY;

				// 减去偏移量
				this.x -= x;
				this.y -= y;

				if (this.drag) {
					// 如果在移动中，改变记录的位置
					this.mouseState.downX -= x;
					this.mouseState.downY -= y;
				}
			}

			// 调用矫正函数
			this.__correctPosition();

			this.rapidZoom = true;
			this.timer3 = setTimeout(() => {
				this.rapidZoom = false;
				this.__correctPosition();
			}, 50);
		},
		// 矫正位置
		__correctPosition() {
			clearTimeout(this.timer1);
			clearTimeout(this.timer2);

			if (this.drag || this.rapidZoom) return;

			this.__transitionClass();

			this.timer1 = setTimeout(() => {
				let t = false; // 是否需要添加过度类名
				let tmp: number;
				const viewfinderSize = this.innerConfig.viewfinderSize;

				// 上
				if (this.y > 0) {
					this.y = 0;
					t = true;
				}
				// 左
				if (this.x > 0) {
					this.x = 0;
					t = true;
				}

				// 右
				if ((tmp = viewfinderSize - (this.__width + this.x)) > 0) {
					this.x += tmp;
					t = true;
				}
				// 下
				if ((tmp = viewfinderSize - (this.__height + this.y)) > 0) {
					this.y += tmp;
					t = true;
				}

				if (t) {
					this.__transitionClass(true);
					this.timer2 = setTimeout(() => this.__transitionClass(), this.CTT);
				}
			}, this.CWT);
		},
		// 鼠标按下
		__onMouseDown(event: MouseEvent) {
			// 按下的不是左键则不动作
			if (event.button !== 0) return;

			event.preventDefault();

			this.__transitionClass();

			const dom = event.target as HTMLImageElement;

			if (!dom || dom.tagName !== 'IMG') return;
			this.drag = true;
			const startX = event.clientX;
			const startY = event.clientY;
			this.mouseState.downX = this.x;
			this.mouseState.downY = this.y;

			const onmove = (event: MouseEvent) => {
				this.x = event.clientX - startX + this.mouseState.downX;
				this.y = event.clientY - startY + this.mouseState.downY;
				event.preventDefault();
			};

			const onup = () => {
				window.removeEventListener('mousemove', onmove);
				window.removeEventListener('mouseup', onup);
				this.drag = false;
				this.__correctPosition();
			};

			window.addEventListener('mousemove', onmove);
			window.addEventListener('mouseup', onup);
		},
		// 鼠标滚动
		__onWheel(event: WheelEvent) {
			event.preventDefault();

			this.__zoom(event.deltaY < 0);
		},
		// 鼠标移动
		__onMouseMove(event: MouseEvent) {
			// 计算缩放中心点偏移百分比
			this.mouseState.zoomOffsetX = event.offsetX / this.__width;
			this.mouseState.zoomOffsetY = event.offsetY / this.__height;
		},
		// 遮挡区鼠标按下事件
		__onCropperMouseDown() {
			if (!this.innerConfig.cropperLongPressOpaque) return;

			clearTimeout(this.timer4);
			// 延迟
			this.timer4 = setTimeout(() => (this.cropperOpaque = true), 200);

			const cp = () => {
				clearTimeout(this.timer4);
				this.cropperOpaque = false;
				window.removeEventListener('mouseup', cp);
			};
			window.addEventListener('mouseup', cp);
		},
		/**
		 * 操作过度类名
		 * @param s 添加 / 删除
		 */
		__transitionClass(s = false) {
			const dom = this.$refs.img as HTMLImageElement;
			const className = '__transition';
			if (dom) {
				if (s) {
					dom.classList.add(className);
				} else {
					dom.classList.remove(className);
				}
			}
		},
		// 获取文件名
		__getFilename(name: string | undefined) {
			// 传入的文件名
			if (name) return name;
			// 默认文件名
			if (this.innerConfig.defaultFilename) return this.innerConfig.defaultFilename;
			// File 对象的 name 属性
			const arr = this.filename.split('.');

			if (arr.length > 1) arr.splice(arr.length - 1, 1);

			return (name = arr.join('.'));
		},

		// 注入数据
		inject(file: File) {
			if (file) this.__init(file);
		},
		// 截取
		capture(dataType: DataType = 'all', format?: ImageFormat, name?: string) {
			return new Promise<Result>((resolve, reject) => {
				if (!this.src) return resolve(null);

				const config = this.innerConfig;
				const viewfinderSize = config.viewfinderSize; // 取景器大小
				const zoomRatio = this.__width / this.naturalWidth; // 实际缩放比例
				const x = Math.round(Math.abs(this.x / zoomRatio)); // 截取坐标 x 轴
				const y = Math.round(Math.abs(this.y / zoomRatio)); // 截取坐标 x 轴
				const l = Math.round(Math.abs(viewfinderSize / zoomRatio)); // 截取的宽高
				let n = l; // 实际绘制的宽高

				// 判断 limitImageMaxSize 配置项是否生效
				if (config.limitImageMaxSize > 0 && n > config.limitImageMaxSize) {
					n = config.limitImageMaxSize;
				}

				// 创建 canvas
				const canvas = document.createElement('canvas');
				const ctx = canvas.getContext('2d');

				if (!ctx) return reject(new Error('Canvas 2D Context is not supported'));

				// 设置画板大小
				canvas.width = n;
				canvas.height = n;

				// 在 canvas 上绘制图片
				try {
					ctx.drawImage(this.$refs.img as HTMLImageElement, x, y, l, l, 0, 0, n, n);
				} catch (error) {
					return reject(new Error('Can not draw image'));
				}

				// 拼接文件名
				const filename = this.__getFilename(name) + '.' + (format = format || config.imageFormat || 'png');
				// 文件类型
				const filetype = extMap[format];

				// 转 Blob，再转 File
				canvas.toBlob(async (blob) => {
					if (!blob) return reject(new Error('Can not get blob'));

					try {
						// 创建 file 对象
						const file = new File([blob], filename, { type: filetype });

						// 判断是否需要 获取 base64
						if (dataType === 'all' || dataType === 'base64') {
							const data = await fileToBase64(file);
							if (dataType === 'all') {
								resolve({ base64: data, file });
							} else {
								resolve(data);
							}
						} else if (dataType === 'file') {
							resolve(file);
						}
					} catch (error) {
						return reject(error);
					}
				}, filetype);
			});
		},
		// 移动
		move(orientation: Orientation) {
			switch (orientation) {
				case 't': // 上
					this.y -= this.moveSpeed;
					break;
				case 'r': // 右
					this.y += this.moveSpeed;
					break;
				case 'b': // 下
					this.x += this.moveSpeed;
					break;
				case 'l': // 左
					this.x -= this.moveSpeed;
					break;
			}
		},
		// 缩放
		zoom(s: boolean, zoX: number, zoY: number) {
			this.__zoom(s, zoX, zoY);
		},
	},
	watch: {
		config() {
			this.__initConfig();
		},
	},
	computed: {
		__width(): number {
			return Math.floor(this.initialWidth * (this.scale + 1));
		},
		__height(): number {
			return Math.floor(this.initialHeight * (this.scale + 1));
		},
	},
});
</script>

<style lang="scss" src="./style.scss"></style>
